﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security;
using Microsoft.CSharp;
using Xiety.CompileGenerator.Properties;

namespace Xiety.CompileGenerator
{
	public sealed class CompileInAnotherDomain : MarshalByRefObject
	{
		private const string DefaultCompileOptions = @"/target:library /optimize";
		private CompileOptions _options;

		public CompileInAnotherDomain()
		{
			AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;
			AppDomain.CurrentDomain.DomainUnload += CurrentDomainDomainUnload;
		}

		private void CurrentDomainDomainUnload(object sender, EventArgs e)
		{
			AppDomain.CurrentDomain.DomainUnload -= CurrentDomainDomainUnload;
			AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomainAssemblyResolve;
		}

		private Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
		{
			if (args.Name == Assembly.GetExecutingAssembly().FullName)
				return Assembly.GetExecutingAssembly();

			return new AssemblyName(args.Name)
				.With(an => _options.References.FirstOrDefault(r => r.IndexOf(an.Name, StringComparison.OrdinalIgnoreCase) != -1))
				.With(Assembly.LoadFrom);
		}

		[SecurityCritical]
		public CompileOutput Generate(CompileOptions opts)
		{
			if (opts == null) throw new ArgumentNullException("opts");
			_options = opts;

			return Compile()
				.If(cr => cr.Errors.Count > 0, CollectErrors, RunGenerator);
		}

		private CompileOutput RunGenerator(CompilerResults compileResults)
		{
			var output = new CompileOutput();

			try
			{
				dynamic data = CreateInstanceFromCompiledResults(compileResults);
				data.Run();

				dynamic gen = CreateInstanceFromTypeName(_options.GeneratorTypeName);
				output.Results = gen.Generate(_options.FileName, data);
			}
			catch (Exception e)
			{
				output.Errors = new List<CompileOutputError> { new CompileOutputError(0, 0, e.Message) };
				output.Results = null;
			}

			return output;
		}

		private static object CreateInstanceFromTypeName(string typeName)
		{
			return Type.GetType(typeName)
				.Throws(Resources.ErrorGeneratorType, typeName)
				.With(Activator.CreateInstance);
		}

		private static object CreateInstanceFromCompiledResults(CompilerResults compileResults)
		{
			var assembly = compileResults.CompiledAssembly;
			return assembly.GetTypes().FirstOrDefault()
				.With(t => assembly.CreateInstance(t.FullName));
		}

		private static CompileOutput CollectErrors(CompilerResults compileResults)
		{
			return new CompileOutput
			{
				Errors = compileResults.Errors.Cast<CompilerError>()
							.Select(er => new CompileOutputError(er.Line, er.Column, er.ErrorText))
							.ToList(),
			};
		}

		[SecurityCritical]
		private CompilerResults Compile()
		{
			var cp = new CompilerParameters
			{
				CompilerOptions = DefaultCompileOptions,
				GenerateExecutable = false,
				GenerateInMemory = true
			};

			cp.ReferencedAssemblies.AddRange(_options.References.ToArray());
			cp.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().ManifestModule.FullyQualifiedName);

			using (var provider = new CSharpCodeProvider())
			{
				return provider.CompileAssemblyFromSource(cp, _options.Code);
			}
		}
	}

	[Serializable]
	public class CompileOutput
	{
		public string Results { get; set; }
		public List<CompileOutputError> Errors { get; set; }
	}

	[Serializable]
	public class CompileOutputError
	{
		public int Line { get; set; }
		public int Column { get; set; }
		public string ErrorText { get; set; }

		public CompileOutputError(int line, int column, string errorText)
		{
			this.Line = line;
			this.Column = column;
			this.ErrorText = errorText;
		}
	}
}
